
using System;
using System.Configuration;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using ScrewTurn.Wiki.PluginFramework;
using System.Text;

namespace ScrewTurn.Wiki {

    /// <summary>
    /// Search Tools.
    /// </summary>
    public static class SearchTools {

		private const int TitleRelevance = 10;
		private const int ContentRelevance = 6;

        /// <summary>
        /// Searches through the Title of the Pages.
        /// </summary>
        /// <param name="query">The search query.</param>
        /// <returns>The search matches.</returns>
        public static List<SearchMatch> Search(string query) {
			return Search(query, Pages.Instance.AllCategories.ConvertAll<string>(delegate(CategoryInfo ci) { return ci.Name; }), true);
        }

		/// <summary>
		/// Searches through the Title of the Pages.
		/// </summary>
		/// <param name="query">The search query.</param>
		/// <param name="categories">The Categories list in which to filter the search.</param>
		/// <param name="searchUncategorized">True to search through uncategorized Pages.</param>
		/// <returns>The search matches.</returns>
		public static List<SearchMatch> Search(string query, List<string> categories, bool searchUncategorized) {
			
			List<SearchMatch> result = new List<SearchMatch>(30);
			Regex r = PrepareRegex(query);

			PageContent content;
			foreach(PageInfo info in Pages.Instance.AllPages) {
				content = Content.GetPageContent(info, true);

				MatchCollection matches = r.Matches(content.Title);

				if(matches.Count > 0) {
					CategoryInfo[] cats = Pages.Instance.GetCategoriesPerPage(info);
					int relevance = ComputeRelevance(matches, TitleRelevance);
					if(cats.Length == 0 && searchUncategorized) {
						result.Add(new SearchMatch(info, relevance, ""));
					}
					else {
						for(int k = 0; k < cats.Length; k++) {
							if(categories.Contains(cats[k].Name)) {
								result.Add(new SearchMatch(info, relevance, ""));
								break;
							}
						}
					}
				}
			}

			return result;
		}

		/// <summary>
		/// Searches through the Title and Content of the Pages.
		/// </summary>
		/// <param name="query">The search query.</param>
		/// <param name="categories">The Categories list in which to filter the search.</param>
		/// <param name="searchUncategorized">True to search through uncategorized Pages.</param>
		/// <returns>The search matches.</returns>
		public static List<SearchMatch> SearchFullText(string query, List<string> categories, bool searchUncategorized) {

			List<SearchMatch> result = Search(query, categories, searchUncategorized);
			Regex r = PrepareRegex(query);

			PageContent content;
			foreach(PageInfo info in Pages.Instance.AllPages) {
				content = Content.GetPageContent(info, true);

				string input = ParseHtml(Content.GetFormattedPageContent(info, true, false));
				MatchCollection matches = r.Matches(input);
				CategoryInfo[] cats = Pages.Instance.GetCategoriesPerPage(info);
				if(matches.Count > 0) {
					int relevance = ComputeRelevance(matches, ContentRelevance);
					if(cats.Length == 0 && searchUncategorized) {
						MergeMatches(result, info, relevance, BuildFormattedExcerpt(matches, input));
					}
					else {
						for(int k = 0; k < cats.Length; k++) {
							if(categories.Contains(cats[k].Name)) {
								MergeMatches(result, info, relevance, BuildFormattedExcerpt(matches, input));
								break;
							}
						}
					}
				}
			}

			// Sort by relevance (reverse)
			result.Sort(delegate(SearchMatch x, SearchMatch y) { return y.Relevance.CompareTo(x.Relevance); });

			// Correct relevance
			if(result.Count > 1) {
				int maxRelevance = result[0].Relevance;
				for(int i = 0; i < result.Count; i++) {
					result[i].Relevance = (int)Math.Round((double)result[i].Relevance / (double)maxRelevance * 100D);
				}
			}
			else if(result.Count == 1) result[0].Relevance = 100;

			return result;
		}

		/// <summary>
		/// Computes the relevance of a search matches.
		/// </summary>
		/// <param name="matches">The matches.</param>
		/// <param name="itemValue">The value of each match.</param>
		/// <returns>The relevance.</returns>
		private static int ComputeRelevance(MatchCollection matches, int itemValue) {
			// Relevance = # of matches * itemValue + # of contiguous matches * itemValue
			int relevance = matches.Count * itemValue;

			for(int i = 1; i < matches.Count; i++) {
				if(matches[i].Index - (matches[i - 1].Index + matches[i - 1].Length) < 3) relevance += itemValue;
			}

			return relevance;
		}

		/// <summary>
		/// Merges a new match with the existing ones.
		/// </summary>
		/// <param name="matches">The existing matches.</param>
		/// <param name="pageInfo">The matched page.</param>
		/// <param name="relevance">The relevance.</param>
		/// <param name="excerpt">The excerpt.</param>
		private static void MergeMatches(List<SearchMatch> matches, PageInfo pageInfo, int relevance, string excerpt) {
			SearchMatch temp = matches.Find(delegate(SearchMatch match) { return match.PageInfo == pageInfo; });
			if(temp == null) matches.Add(new SearchMatch(pageInfo, relevance, excerpt));
			else {
				temp.Relevance += relevance;
				temp.FormattedExcerpt = excerpt;
			}
		}

		/// <summary>
		/// Prepares a Regex from a query.
		/// </summary>
		/// <param name="query">The query.</param>
		/// <returns>The Regex.</returns>
		private static Regex PrepareRegex(string query) {
			//string[] words;
			Regex r = null;

			query = query.Trim().Replace("  ", " ");

			StringBuilder sb = new StringBuilder();
			// Replace Regex special characters, adding support for wildcards
			sb.Append(EscapeRegexChars(query).Replace("\\*", ".*").Replace("\\?", "."));

			if(query.StartsWith("\"") && query.EndsWith("\"")) {
				// Exact phrase
				sb.Remove(sb.Length - 1, 1);
				sb.Remove(0, 1);
				sb.Insert(0, @"\b(");
				sb.Append(@")\b");
				sb.Replace(" ", @")\b[\s\r\n]+\b(");
				r = new Regex(sb.ToString(), RegexOptions.IgnoreCase);
			}
			else {
				sb.Insert(0, @"(\b(");
				sb.Append(@")\b)");
				sb.Replace(" ", @")\b)|(\b(");
				r = new Regex(sb.ToString(), RegexOptions.IgnoreCase);
			}
			return r;
		}

		/// <summary>
		/// Removes all HTML tags from a text.
		/// </summary>
		/// <param name="html">The input HTML.</param>
		/// <returns>The extracted plain text.</returns>
		private static string ParseHtml(string html) {
			StringBuilder sb = new StringBuilder(Regex.Replace(html, "<[^>]*>", " "));
			sb.Replace("&nbsp;", " ");
			sb.Replace("  ", " ");
			return sb.ToString();
		}

		/// <summary>
		/// Escapes all regex-reserved characters in a string.
		/// </summary>
		/// <param name="input">The string to process.</param>
		/// <returns>The escaped string.</returns>
		private static string EscapeRegexChars(string input) {
			StringBuilder sb = new StringBuilder(input);
			sb.Replace(@"\", @"\\"); // This must be done BEFORE all others
			sb.Replace("(", @"\(");
			sb.Replace(")", @"\)");
			sb.Replace("[", @"\[");
			sb.Replace("]", @"\]");
			sb.Replace("{", @"\{");
			sb.Replace("}", @"\}");
			sb.Replace("/", @"\/");
			sb.Replace("^", @"\^");
			sb.Replace("$", @"\$");
			sb.Replace("?", @"\?");
			sb.Replace("+", @"\+");
			sb.Replace("*", @"\*");
			sb.Replace("#", @"\#");
			sb.Replace(".", @"\.");
			sb.Replace(":", @"\:");
			sb.Replace("<", @"\<");
			sb.Replace(">", @"\>");
			sb.Replace("=", @"\=");
			return sb.ToString();
		}

		/// <summary>
		/// Builds the formatted excerpt for a search match.
		/// </summary>
		/// <param name="matches">The regex matches.</param>
		/// <param name="input">The original input text.</param>
		/// <returns>The formatted excerpt.</returns>
		private static string BuildFormattedExcerpt(MatchCollection matches, string input) {
			// Highlight all the matches in the original string, then cut it

			int shift = 100;
			int maxLen = 600;
			string highlightOpen = "<b>";
			string highlightClose = "</b>";

			StringBuilder sb = new StringBuilder(input);

			for(int i = 0; i < matches.Count; i++) {
				Match match = matches[i];

				sb.Insert(match.Index + i * (highlightOpen.Length + highlightClose.Length), highlightOpen);
				sb.Insert(match.Index + match.Length + highlightOpen.Length + i * (highlightOpen.Length + highlightClose.Length), highlightClose);

			}

			bool startsAtZero = false, endsAtEnd = false;

			int start = matches[0].Index - shift;
			if(start < 0) {
				start = 0;
				startsAtZero = true;
			}
			int len = matches[matches.Count - 1].Index + matches[matches.Count - 1].Length + shift - matches.Count * (highlightOpen.Length + highlightClose.Length) - start;
			if(start + len >= sb.Length) {
				len = sb.Length - start;
				endsAtEnd = true;
			}
			else if(len > maxLen) len = maxLen;

			string result = sb.ToString();

			// Cut string without breaking words
			while(start > 0 && result[start] != ' ') {
				start--;
				len++;
			}
			while(start + len < result.Length && result[start + len] != ' ') len++;

			result = sb.ToString().Substring(start, len);

			if(!startsAtZero) result = "[...] " + result;
			if(!endsAtEnd) result += " [...]";

			return result;
		}

	}

	/// <summary>
	/// Represents a match resulting from a full-text search.
	/// </summary>
	public class SearchMatch {

		private PageInfo pageInfo;
		private int relevance;
		private string formattedExcerpt;

		/// <summary>
		/// Initializes a new instacne of the <b>SearchMatch</b> class.
		/// </summary>
		/// <param name="pageInfo">The matched page.</param>
		/// <param name="relevance">The match relevance.</param>
		/// <param name="formattedExcerpt">A formatted excerpt for the match, if any.</param>
		public SearchMatch(PageInfo pageInfo, int relevance, string formattedExcerpt) {
			this.pageInfo = pageInfo;
			this.relevance = relevance;
			this.formattedExcerpt = formattedExcerpt;
		}

		/// <summary>
		/// Gets the matched page.
		/// </summary>
		public PageInfo PageInfo {
			get { return pageInfo; }
		}

		/// <summary>
		/// Gets or sets the match relevance.
		/// </summary>
		public int Relevance {
			get { return relevance; }
			set { relevance = value; }
		}

		/// <summary>
		/// Gets or sets the formatted excerpt, if any.
		/// </summary>
		public string FormattedExcerpt {
			get { return formattedExcerpt; }
			set { formattedExcerpt = value; }
		}

		/// <summary>
		/// Gets a value indicating whether the formatted excerpt is available.
		/// </summary>
		public bool IsExcerptAvailable {
			get { return !string.IsNullOrEmpty(formattedExcerpt); }
		}

	}

}
